{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "O5pKTpHfeJ0t"
   },
   "source": [
    "# CFO Correction Network\n",
    "\n",
    "This notebooks contains two network architectures to **learn to correct Carrier Frequency Offset (CFO)**, given preamble and preamble convolved data.\n",
    "\n",
    "* **First Approach**: Feedforward network\n",
    "    * Pros: Faster inference\n",
    "    * Cons: Fixed Preabmle Length\n",
    "* **Second Approach**: Recurrent Neural Network network\n",
    "    * Pros: variable preamble length\n",
    "    * Cons: Slower\n",
    "    \n",
    "## Environment Setup\n",
    "* Tensorflow\n",
    "* Keras\n",
    "* Commpy\n",
    "* Pydot\n",
    "*  `graphviz` for visualization. `sudo apt-get install graphviz`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import packages from other direction. Itis necessary if the project is structured as:\n",
    "# my_project\n",
    "# ├── notebooks\n",
    "# │   └── current_notebook.ipynb\n",
    "# ├── local_python_package\n",
    "# │   ├── __init__.py\n",
    "# │   ├── models.py\n",
    "# ├── README.md\n",
    "import os\n",
    "import sys\n",
    "module_path = os.path.abspath(os.path.join('..'))\n",
    "if module_path not in sys.path:\n",
    "    sys.path.append(module_path)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     },
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1245,
     "status": "ok",
     "timestamp": 1532646976703,
     "user": {
      "displayName": "Dat Nguyen",
      "photoUrl": "//lh3.googleusercontent.com/-irIcNYd-KIw/AAAAAAAAAAI/AAAAAAAAAEs/NlM8kG6RL4Q/s50-c-k-no/photo.jpg",
      "userId": "108917076199533451784"
     },
     "user_tz": 420
    },
    "id": "Ca66rN5HeJ01",
    "outputId": "9d2d668a-4420-4c59-cfe6-fec222a4c932"
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Using TensorFlow backend.\n"
     ]
    }
   ],
   "source": [
    "import multiprocessing as mp\n",
    "import numpy as np\n",
    "import tensorflow as tf\n",
    "from radioml.dataset import RadioDataGenerator\n",
    "from sklearn.metrics import mean_squared_error\n",
    "\n",
    "# For visualization\n",
    "from IPython.display import SVG, display\n",
    "from keras.utils.vis_utils import model_to_dot\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "import pylab"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Define Parameters for this experiment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "DATA_LEN = 200\n",
    "PREAMBLE_LEN = 40\n",
    "CHANNEL_LEN = 1\n",
    "\n",
    "SNR_TRAIN = 20.0\n",
    "OMEGA_TRAIN = 1/50"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Define training_set, validation_set"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define Radio\n",
    "radio = RadioDataGenerator(DATA_LEN,PREAMBLE_LEN, CHANNEL_LEN, modulation_scheme='QPSK')\n",
    "\n",
    "training_generator   = radio.cfo_data_generator(OMEGA_TRAIN, SNR_TRAIN, \n",
    "                                                batch_size=128, num_cpus=8)\n",
    "\n",
    "validation_generator = radio.cfo_data_generator(OMEGA_TRAIN, SNR_TRAIN, \n",
    "                                                batch_size=128, seed=2018)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl8AAAEICAYAAABh3JHPAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvhp/UCwAAIABJREFUeJzt3Xl8U1X6x/HPkwYoUKBAWQXFjXHBvaKIOoyDio6KOu7jguOGKCLujjOKuK9YFVFEx21UXAZFxVFREXfEHXfkh7JDgQIFCqQ5vz9yW9I2SdM2a/t9v155kXvOyb1Pwu3Jk3vPPdecc4iIiIhIavjSHYCIiIhIU6LkS0RERCSFlHyJiIiIpJCSLxEREZEUUvIlIiIikkJKvkRERERSSMmXiIiISAop+UoiM+tlZs7M/FHqR5nZU6mOKxka8l4s5N9mttLMZiQ6NqkbM9vSzErNLCfdsUhmakp9W23MrL+Z/eL9zRyd7niaomzssxpF8mVmc81svffhLzGzx8wsL91xpYqZHWpm081sjZktM7P3zOyodMdVB/sDBwM9nHN9U7HBWJ+ZmQ0xs3Jvf6p43B/22v3M7B3vtavM7BUz26mW7XUzs0fMbJH3uh/N7Hoza53s91ob7+9nYMWyc+5351yec648nXGJ+rYs6dtGA/d7fzMvmdk0Mzs70RtRn1UltqzvsxpF8uU50jmXB+wJFAL/rN7AO8LSmN4zZnYc8DzwBNAD6AJcCxyZzrjqaCtgrnNubV1fGO2Xdy2viecz+9j7Y654XOi9th/wJvAy0B3YGvga+NDMtomyvQ7Ax0BLoJ9zrg2hZDMf2LaOsdfYhxvjfi1VqG/L7L5tK+C7RK0s0tEb9VmNkHMu6x/AXGBg2PIdwKve82nATcCHwHpgO6Ad8AiwCFgA3AjkeO23Bd4BlgPFwH+A/Grbuhz4BljrracL8DqwBpgKtPfa9gIccC6w0NveZWHrGgU8Fba8L/ARUELoj2NALe/bgN+By2O08RHqrH8DlhL6421XLb4zvPUUA9d4dd29z6tD2Lr28No0i7CdUcALwETvc/gC2C2svjvwIrAM+D/gIq/8LKAMKAdKgeu98nOA2cAKYDLQPWxdDrgA+AX4P69sB+Atr/1PwAkN+MyGAB9EqXsfeCBC+evAE1FecyPwLeCLsc39gM+AVd6/+4XVTaPmPhypLOp+HfaZ/uD9/3xP6Mv8SSDoraMUuCJsv/CH/d9N9j7b2cA51f7fn/P2qzWEvoQK090nNJYH6tsyoW/rSygRKfHe5/1Ac6/u12p/P7cQ6svKvOX7vXZR+yfgMWAcMMX73AfW47MYgvqsrOqz0t65JORNhHVQQE/vw7whbCf4HdgZ8APNgEnAQ0BroDMwAzjPa78doQy/BdAJmA7cU21bnxDqlLYg9Ef/BaE/3lxCndt1XtuKHeIZb1u7EEo+KmIdhddBeetaDhxOqFM52FvuFON97+Ctf+sYbf7u7XzbAHnAf4Enq8X3MKFfOLsBG4Advfp3qu20dwAPRtnOKGATcJz3GV9GKMlq5r2fzwn9UmvuxTIHONR77RDCOg7gIEId4Z7e/8N9wPSwekeoI+vgxd0amAec6f0fV3SkO9XzM6sST1h5K0Id658i1J0JLIqyvk/wksoo9R2AlcBpXvwne8sdY+zDkcpi7dfHE+rc9ibUmW8HbFX976faflHRkU0HHiC0f+9OaB8+KOz/vYzQfptD6Mvnk3T3CY3lgfq2TOjb9iKUPPq99f4AXBzp/yjs/+XssOWY/ROh5GsV0N/7fHLr8VkMQX1WVvVZae9cEvImQv8RpYR+mfzmfegtw3aC0WFtuxD6I2wZVnYy8G6UdR8NfFltW38LW34RGBe2PBx4qdoOsUNY/e3AI2E7QUUHdSVexxHW9g3gjBjvu7+3/twYbd4GhoUt/4FQkuQPi69HWP0M4CTv+dnAO95zI9SBHBhlO6PCd2BCncgi4ABgH+D3au2vBv7tPR9C1eTrEeD2sOU8L+Ze3rKr+EPylk8E3q+2/ofwvijq8ZkNAQLe/lTx2JfQ4f4q/59hrxkEbIqyvl+AoTG2dxowo1rZx8CQSPtwffZrb18aEePvJ2JHRugLvxxoE1Z/C/BY2P/71LC6nYD1dfn71SP6A/Vtae/bImz3YmBStc8tVvIVs38ilHxFPAJVh89iCOqzsqrPqvN4mQx2tHNuapS6eWHPtyKUcS8ys4oyX0UbM+sCFBFKGtp4dSurrW9J2PP1EZarD4gN3/5vhH4lVrcVcLyZhZ/Dbwa8G/EdhSz3/u1G6ChTJN29bYZv309ox6+wOOz5OjbH/yJwn5l1A3oTOtT7fox4Kt+ncy5oZvO97Tugu5mVhLXNibGu7oR+cVesq9TMlhP6BT23+rYIfXb7VFu/n9Dh6eri+cwglEjuH17gDTQNeq/9sVr7boR+zUay3KuPpvr/Ed7yFmHL86gp7v2aUIf0a4wYYsW2wjm3plpshWHL1fefXDPzO+cC9die1KS+LbKU9G1m1hu4m9A+38rbxucxYq8unv4p0t93BfVZdZMVfVZTGfDmwp7PI5RtFzjn8r1HW+fczl79zV77XZxzbYFTCf0yaoieYc+3JDRGorp5hH4d5oc9Wjvnbo2x3p+81/01RpuFhHby8O0HqNqpRuScW0looOaJwCnAs877qRBF5fv0BlP28LY/j9DYrPD31sY5d3g8MXsdSEdCh6Arwwt7Pg94r9r685xz50dYdzyfWUQudEHAx4QOh1d3AqFf4pFMBY6JMcC0+v8RhP6for3fSGW17dfziD5QNtb/6UKgg5m1iRGbpI/6tqrbT0bfNo5Q4rK997n9g9ifW/X1xNM/xfobVJ8Vez2RYsv4PqupJF+VnHOLCP3R3WVmbc3MZ2bbmtkfvSZtCB3mX2VmWxAagNpQ/zKzVma2M6Hz7BMjtHkKONK7nDjHzHLNbICZ9YjxXhxwibf+M8Pez/5mNt5r9gww0sy29i5RvxmYWIcM/2ngdEJjuZ6upe1eZnasdwXixYT+sD4hdLh/jZldaWYtvffXx8z2jrKeZ4AzzWx3M2vhxfypc25ulPavAr3N7DQza+Y99jazHas3jPMzi+Uq4Awzu8jM2phZezO7EegHXB/lNXcDbYHHzWwrADPbwszuNrNdCQ207W1mp5iZ38xOJHQo/NU44ql4X7Xt1xOAy8xsL+9Ko+0qYiH0ZRXxqifn3DxCA6Vv8fbJXQldJNEk5nDKJurbkta3tQFWA6VmtgMQ6UdduOp/T3H3T5Goz2qcfVaTS748pxMa+P09ocPuL7D5EOv1hAZ6rwJeIzSIs6HeIzQw9G3gTufcm9UbeDvMYEK/qpYRyvovp5b/I+fcC4R+vf2dUMa/hNAVIy97TR4ldHh7OqFD1mWExm7EazKwPbDYOfd1LW1f9mKpGIh5rHNukwvNvXIEoYGP/0foUPcEQle6RHpPU4F/ETo1sIjQr5+Tom3UO7x8iNdmIaFDyrcRGlgcqX1tn1lUzrkPgEOBY73YfiM0gHZ/59wvUV6zgtCVQZuAT81sDaF9YRUw2zm3nNDncymhw/1XAEc456KdEogm6n7tnHue0JVGTxO6wuclQoNmITQe4p9mVmJml0VY78mExlQsJDRA9roYp8EkvdS3xS/evu0yQkfH1hAawB8pwQxXBBxnoUmj761r/xSJ+qzG12dZ7LNIIiIiIpJITfXIl4iIiEhaNKarHRstMyuNUnWYcy7W1YciIhlLfZs0VTrtKCIiIpJCGX3kq6CgwPXq1SvdYUgazFkWus3jNp3Sfg9XSaHPP/+82DnXKd1xJIL6r6ZFfZZA/H1YRidfvXr1YubMmekOQ9LgxIc+BmDief3SHImkkplVn7gxa6n/alrUZwnE34dpwL2IiIhICin5EhEREUkhJV8iIiIiKaTkS0RERCSFlHyJiIiIpJCSL0moXxcWc/JNTzH42n8z+aPv0h2OSKNQUlrGxGlf8vO8pekORUQSIKOnmpDsculDr/DuV7Mrl0c9+SZ3vTiN9+66II1RiWS3I66ZwMIVayqX/T5j8g1n0bVDmzRGJSINoSNfkhALikuqJF4V1qzbyD8f+18aIhLJfn+/49kqiRdAIOg46l+PpCkiEUkEJV+SEP/6d/QE683PfkphJCKNx1dzFkUsDwSdTkGKZDElX5IQZZvKo9YFdf9QkYT7es7CdIcgIvWk5EsS4uJjD4hat+vWXVMYiUjjYRa97uC9dkhdICKSUBpwLwnRd4ct6d6hTY3xKWbwwPDj0hSVSHb7+6F9eeR/M2qUd+/Qhvy83Mrl0U++yZuf/4w/x8e5R/TjlD/t0aDtTvrgW+7573Q2bAyw41ZduG/40eTl5tb+QpEkCpZ9DiXDgBKgGbQ4CjZ9BMHV0KwQ2j2Az58daU12RClZ4dWbzmbsyx/y9DtfECgPslfvHtw7bDD+LPljEMk0FwzuD8Cjb8yg4uz97tt049HLTwIgEAjQ/+KxbCoPVr7mzuem8fy0L5l0/d8rywKBABc98DIzfvwd52C77gU8cvnxEROq8+55gc9+mle5/PWcRRw4chxv3nIOBfl5yXibIrUKrn8dVo0IK9kIG17YvLhpGhTvRLDgQ3z+TqkOr870rSgJdcHg/pVfGCLScLH+pq54+LUqiVeF35auYsaPv9N3hy0BOPCScZRtClTW/7KwmANHjuOjMcPJzd38NbCytLRK4hXutNue5vVbzm3IWxGpv1WXxdeu+Fjo+n5yY0mAhIz5MrNHzWypmc2KUj/AzFaZ2Vfe49pEbFdEpCn78Lu5UevueG4aAE++NbNK4hXu4odeqrJ89LWPR13fkpK1dY5PJHE2xdluSVKjSJREDbh/DBhUS5v3nXO7e4/RCdquNBKlZWU8MPlDzYovkiA5vtBo/Wff+zpqm69+WVD5/LB/PMya9RuTHpeIJOi0o3Nuupn1SsS6pOk5+66JfDF782Xzo558kw7b7ErHtq3SGJVI5ju48A9M+fSHiHX/PHUgAG1ymxN5tjBo1iwHgOKSUpasLI25LV+sSy9Fks26g4tvepVgoASfPz/JATVMKqea6GdmX5vZ62a2cwq3KxnswVc/rpJ4VZi7ZAVO84OJxHTjkEG0atEsYt3IcZOZt7SEm/5+WNTXn3v4vgC8/tmPtW5r56061y9IkQYIBgIEV40Caw/E+QOgeL9khpQQqUq+vgC2cs7tBtwHvBStoZmda2YzzWzmsmXLUhSepMtjb9S8jL7C3CUrUxiJSHb64J4LGX50zQH5y1evY/B1/6Z1bgsGFf6hRv323Tty2sGFAOyx/Ra1bqfogmMbHqxIHQTLPoTinWD90xD8DnCAgW9XaHlqjFcGQldHZrCUJF/OudXOuVLv+RSgmZkVRGk73jlX6Jwr7NQp8y8XlYbZFKh5pVaFDVEGCYtIVZ3abZ4CovoR42H3vsjR/fvQtmULAAzH8MH9mfiv0yvb9OnVrXKMWCR3Dz2yyrxiIilR8vcqi6F920FwIWyoOZaxyr6//rUkB9cwKZlqwsy6Akucc87M+hJK+panYttSNx9/P5cRD7xMwLt8vUWzHB6//CR690zOKYeObVtRvHpdxLpO7VonZZsijc1TUz8HYNFn/6N843q22O9ozBujNXfJSoYWvQiEvpzmf/QS//jsDZaWXMGVJ/25ch2v3HAWR/7rEcqDm7/Aundow6s3nZ3CdyISEtxQcaQr5Po7l1OyKsjdowswK4ZgcZX2zjkuubaY/HY+rrusI+T+mUyWqKkmngE+Bv5gZvPN7CwzG2pmQ70mxwGzzOxr4F7gJKcBPRmnuKSUC+6bVJl4AWzYVM5JN/+HQCA5R6HuveDoqHVzl6ykcNgYxr78YVK2LZItFq9Yw6y50YbNQ6f8PJxzlG9cz7Jv32fBRy/VOALmnGPBRy+x7Nv3Kd+4nmenVT1y0LVDGz4bezHP/uNvXH3Sn3jnjvOVeEn6BDfv7845SlYFuXdCCZdcWxxx377k2mLunVBCyaogzhm+Vsc0bPObfiEY+K1B64glUVc7nlxL/f3A/YnYlmz2zZyFXHDff1lbFpr/ZKsu+Tz/z9PqPaP8BfdNilp32fjXuGfY4HqtN5YdtuzCgyP+yshxL7N+Y80EL+jgkf/NYP3GTVx2/ICEb1/EzB4FjgCWOuf6RKg3oAg4HFgHDHHOfZGK2GbNXcSQ2ydWuTn9oYW9ueWsv1Rpd8vZh3HgyHFssV/ox8yyb0OTTFYcAQtPvDrtckBl+bylJfTsXPWqsN49OyftSLdI3GyXzU/NuHt0aKTSvRNKALwjYFYl8bro7PxQefuow8prFSx9GErv2LwM0PZWfK0SO+ZRN9bOUvOWljDkjomViRfAb0tK2O/isfVf57LoA9y/m7u43uutTd8dtuTDouHMuG941DZPv/Nl0rYvTd5jxJ6n8DBge+9xLjAuBTEBcPptz1ZJvADemPkzT741s0pZXm4u5x+xL2bGFvsdTaddDmDZt++z9ps3oiZeAG1aaRyXZKh1N1dZrEjALjo7v8oRsBqJlxmsvbVemwxu/LFK4lVp9VUEA4kdKaXbC2WpYfe9GLE8UB7k6Xe/rNeNdTu0bc3C5asj1vlykj/Hz6c/Rr6tiUgyxTFP4WDgCW+oxCdmlm9m3Zxz0c8DJkCs0+1jJ39UeaVihXP+0o8zD92beyZ9QHFhb5bP7MOEh8bBx28C1Ei8fGYaRC8ZrOaxoepHwCqOglVJvACC9ZwpoeSCGHXnQ8Fz9VtvBDrylaUWRUmSAF764Nt6rfPOc4+IWresZC0lpWX1Wm+8tu0e8QJYkXTbAgj/ZTDfK6shkVPlfPHL/Kh1GwPlEcv9fj+XHT+AW88+gvHjqh4FD0+8AB697IQGxSeSVG2vi1gcnoBVqJJ4AbQ6o37brDaIv4ryxB4cUPKVpZrHGNfVo6Bdvda5w5Zd8Me43PyKh1+p13rj1bVDG5r5I++SW3XO7NmKRSCxU+UM2rvm3FwVWuc2ry0ORo4cWaWs/cJP2L57R44/YBe+GDeSXbfp3qD4RJIp2gz1Facaw1UdhJ+Lr/WJ9dtozlbR6/yJnRteyVeWOv/IflHrbj7zL1HrahPrEtRlq2LffiQRXrvhrBoJYLtWuTz/r9OSvm2RKBYAPcOWe3hlSXX8H3cnJ8otfW4YcmjU11UkXkVFRYwYMYJgMMiIESN495Xn6bb8C646ObMvwReJpvoYr8DCXbjo7C6bx4Dl7AkFDbgWpv0j0evaJfaaQSVfWeq0gwvpu0PPGuVXn/QncnPrP5SvV5cOUeuO7b9L1LpEKcjPY8bYi9mhZ2e6d2zLKzecybt3nV/vKzhFEmAycLqF7AusSvZ4rwpv3T6Udq03j8vyGVx54gAG7LZdxPbVE68xY8ZgZowZM4YRI0ZQVFTEyJEjdesuyRLtK59FHlxfxt2j23DRBUeHErB/vIdbtiPBxb0JLt6d4IZv6rQ1n78T5D8GhN+yqwV0mIzPn9jxkfpGy2IPjjiO0rIyHn/jczrnt+b4P+7e4HWOu+hYDrn64RrlzXJ8NQb4JlPr3Oa0zm3OFgU63SjJ5c1TOAAoMLP5wHV4va9z7kFgCqFpJmYTmmrizFTFlp+Xy7t3nh9X22iJF1CZgAEUFRUBVKkXyUytgJXRr2rEGwN2zS+wIZ97JywGKurXwcrjCBZ8GEqq4uTL3Q+6fpectxNGyVeWy8vN5YLBNe/rVl8F+Xm8eN3pnH3Xc6z0Btj33qKAp66KOZWbSNaKY55CB8S4DCozmBn5+fk1Eq/w+ooELD8/X4mXZIHQ2X0zI7+dr+ZVjR6zjZWD8PPb+arWr/w7dErueOX6UPIlNWzdtSNv3xHfr20RyRyjRo1izfr1UROrigRMiZdkm+su64hzLua+HSkxo3xOCqKrOyVfIiKNwMk3PclP8zdfBVbQthVTbjqrxnhJJV6SPXx4c8wDsfbdFsCGKPVtkhBXw2nAfRb7Zs5Chtz+DEOLXqC4JPlXIopIZjrrzolVEi+A4tXrGHT1hDRFJJIArUfW3gaADdGr2t2YkFASTUe+stRR/3qE+cWbJ1o95OqHGVT4B24+6/A0RiUiDREIBLhs/GvM/HkerVs259q/HUz/PlvX+rovf10YsXxF6XrKygINugJaJF18bc4jaK2h9BZgE2CEroXZGN8Kco/F13Jg8gJsAP1FZoDiklIueuBlFq1YzXbdOzJm2FHk5W6+rLW0rIwTRj/J4pWho1vN/T42BoI11vO/mT9x/pH71bhRrohkvpWlpfz58s1XGq/bsInhY1/ioD22485zj6z3en9euFQTqkrW8uWdCnmnVi4HF/eO3jjvcsjpAcFSaHEMvgyeokinHdNs8kffccjVD/PjvKWsWlvG578s4MCR4/hmzuZfsn+8ZFxl4gVETLwqXP3olKTGKyLJcfptEyOWv/PlbErL6n9rr97dO9f7tSJZxW3A1/IwfK2Pz+jEC5R8pd2oJ9+MWH7O3c8DcNuzb1OX+RDXlsU49y0iGWtBcfT7td79/PSYr+23U+TbonTOb61TjtLIRJ8InJZDUxdGAyn5SqPFK9ZErdtUHjq6Ne3rul0me9agfRoUk4hknhxf7K567PBjKezdo0rZlp3b8b9bzk1mWCKpV/BC5PIWx2b80a5w2RNpE9WhbSuWxHklY5uWzTli352SHJGIJEOvLu2Zu2RlxLpL/zqg1tePH3l8giMSyTw+fw+CBV/AqqGwaRb42kDb20Iz02cRHflKo64dos8/4vd+6d513lFR23QvaIvPIMdnHLzn9rx3d8ZPwi1JFiz7iOCKkQTXPp/uUKSOnrjqJCJNU3TEvjvp1KFIGJ8/D1/Hp/B1/Qpf5/ezLvECHflKu8tOGMCdz02rUX7/8GOAUIJ29mF9mfD6jCr1B++5Pbedc0QqQpQsEAwEoHhPwBuYvfE1gmv+CR1fxdds+7TGJvHJy83l8wdGMvrJN3nvmzm0adWCW886nB227JLu0EQkwZR8JVms2yEAnDxgdw7YeWsuHvcSS1aWslWX9tw/fDDt8/Iq2ww7qj/nHr4PT739Jes2bOTcw/epMWu1NC019qvlB1OZeFXWA8uPgq4/pDw+qb9rTzsk3SGISJLptGMCTPrgWy4b/wrTv/m1SvmoUaMYOXIkLsrlis45Ro4cySMP3MNDI45jy87t+b/FKzj11onMmruoSlu/38+QQ/dm2FH9lXg1cRH3K7dg81PnuOTaYq6/czlQTjCQmfc2ExFpqpR8NcC8pSXsNWwMN/xnKu98OZuLx01mn+FFlJUFcM5RUlJCUVFRxASsIvEqKiri57nzOfiq8fw4byllGwMsWr6a0297lgdf/ThN70wyVTz71SXXFnPvhBJKVgVD9ZuUfImIZJKEHEIxs0eBI4Clzrk+EeoNKAIOB9YBQ5xzXyRi2+n01+sfqzEH16ZAkCOvncBbtw9lzJgxABQVFQEwZswYzKxK4jVixAg+DmyNldecOHX8a58w9Ih+SX8fkj3MLOJ+BT6cK69MvC46O5+7RxeETk02G0Bw/ZuAL+atNoLLh8KmdzYXtDgCX/u7k/huRESapkSdv3oMuB94Ikr9YcD23mMfYJz3b9ZaWVpKIBj5dOLyNeuB6F+U4YnXmDFj2GvYPVG38+Xs+eyxXY+o9dL0RNqv7rrhEi655MqaiZd1g+LN048EVwGtL8LX5sIq6wwWnwKBmVU3tOFVgsvX4ev4YFLfj4hIU5OQ5Ms5N93MesVoMhh4woXOkXxiZvlm1s05tyjGazLakhVr42pX/Yuy4suyIvGKNRgfoJk/p2GBSqNUc78KlV90dgfuHt0BsxxofihsfL3mi9feS7Dlkfj8YbOiV0+8KoQfCRMRkYRI1ZivLYB5YcvzvbIazOxcM5tpZjOXLVuWkuDqY7vuHaPWVU+nwr8oK4QnXi2bR8+B+/TqVu8YpXGLtF/dM76YnG6/4Ov6IwS+i/7iledXPg0G4pvEV0REEiPjBtw758Y75wqdc4WdOnVKdzhR+f1+DuizdcS6c/6yb5XlijFe4cIHSz919SkR13PNKX9OQKTSWNW2XxFcEf3FweKwhdzEByciIlGlKvlaAPQMW+7hlWW1oguO5oyD9yLHFzqC1dzv45pT/lxlkHz1wfXBYJARI0ZUuVpt664dmXHfcAbsug0d2rRk515dePuOc/jrAbum661Jhotnv6JZjFtN+TfvW6H7oeVHbmc68ioikmipmjBqMnChmT1LaKD9qmwe7xVuxLEHMuLYAyPWVf+CrDjVGGkQvt/v5+7zB6csbsle8e5Xd90xDlu+V+SVtLu/6nLBdCjuB4SPZWwHHd9O/BsQEWniEjXVxDPAAKDAzOYD1wHNAJxzDwJTCE0zMZvQVBNnJmK7mSzaFyREvwqytsH3InXdr+66/VVsxYlsTqryoGASPn/VU40+fy50/ZJgYDFseB9aDMDnz9zT/iIi2SxRVzueXEu9A5rUXZ/NjPz8/KhXNYZ/Uebn5yvxkrjUdb/Kad4bun4Z9/p9/q7gPz6hMYuISFW6T00SjRo1Kua9HSu+KJV4SV1ovxIRyW4Zd7VjY1PbF6C+IKU+tF+JiGQvJV8iIiIiKaTkS0RERCSFlHyJiIiIpJCSLxEREZEUUvIl0kQEy94luGo0wQ0x7vkoIiJJp6kmRBq5YGA+FA8EgqGC9U8RpDUUfObdWkhERFJJR75EGrviQ6hMvCqtheWD0hGNiEiTp+RLpBELBuYDgciV7veUxpLJzGyQmf1kZrPN7KoI9UPMbJmZfeU9zk5HnCLSOOicg0hjFvg13RFkPDPLAcYCBwPzgc/MbLJz7vtqTSc65y5MeYAi0ujoyJdIY+bvH6NSs+B7+gKznXNznHMbgWeBwWmOSUQaMSVfIo2Yz+8H/56RK1sNTW0wmWsLYF7Y8nyvrLq/mtk3ZvaCmfWMtCIzO9fMZprZzGXLliUjVhFpBJR8iTRyvoJnocXRbD7S5Ye8y/G1HZnOsLLNK0Av59yuwFvA45EaOefGO+cKnXOFnTp1SmmAIpI9NOZLpAnwtb8duD3dYWSqBUD4kaweXlkl59zysMUJ6MMUkQYzK4QoAAAgAElEQVTQkS8Raeo+A7Y3s63NrDlwEjA5vIGZdQtbPAr4IYXxiUgjoyNfIk1QMLAYSoZC+RJotje0vbvJTrjqnAuY2YXAG0AO8Khz7jszGw3MdM5NBi4ys6MIzduxAhiStoBFJOs1zd5WpAkLlj4MpXdsLtj4Pyj+H8GC6fj8XdMXWBo556YAU6qVXRv2/Grg6lTHJSKNk047ijQ14YlXuOIjqiwGy74kuO6/BANRJmkVEZF60ZEvkSYkuH5qjNrVoTZln0PJKYDzyq8i2Kwfvo4RL/ATEZE60pEvkabEldXepuRkNidenk0fE1ylC/xERBJByZdIE+JrdUSM2lYEV98bvXr9Y4kOR0SkSUpI8qWb0opkkZZnRKlYB+vGxXihxn6JiCRCg5OvsJvSHgbsBJxsZjtFaDrRObe795jQ0O2KSP342l0D+c+Arxs17+9YHuOVeUmMSkSk6UjEkS/dlFYky/hy94IOb1BjbFcs+XclLR4RkaYkEclXwm5KC7oxrUjKlE2Ms6Ef2t6KL/dPSQ1HRKSpSNVUE68AzzjnNpjZeYRuSntQpIbOufHAeIDCwsI6/CwXkTrxbxez2tf15xQFIiLStCTiyFdcN6V1zm3wFicAeyVguyLSAL7c/jEqe8V8bXD5UIKLe3uPPgTXTUpscCIijVgiki/dlFYkW+X/O0JhC+gwJUJ5SHDJQNj0TljJRlh9JcF1/014eCIijVGDTzvqprQi2cuX2x+6/kxw1V0Q+BZanYKv5SFR2wcDy8D9Hrly9b+g1bFJilREpPFIyJgv3ZRWJLv52l0aX8N1T8Wo3JSQWEREGjvNcC8i8Wu2S7ojEBHJekq+RCR+bl30Ol+f1MUhIpLFUjXVhIhkoWCgDFaNgPIfwbpB+RfRG/uapS4wEZEspuRLRCIKls2AklPDShbV8oIlSY1HRKSx0GlHEYmsJNoNuKNocXhy4hARaWSUfIlIDcFAgNg32a7Oh6/dFckKR0SkUVHyJSINVzAz3RGIiGQNJV8iUoPPX4fhoO2K8PnzqhQFA2UES58iWPZhgiMTEcl+GnAvIpHlXQulo2M0aAZtb8DX8rAqpcHlp8KmGZuXAfL/HftekiIiTYiSLxGJyJd3KsFme0DJMHDFYJ2h/Th8zXeI+prgqturJF6VSs6Erj8nMVoRkeyh5EtEovK12Bm6vBf/C9Y/FrUquOqu+G9jJCLSiGnMl4gkUCBG1TepC0NEJIMp+RKRBGoTvarV31IXhohIBlPyJSJxCwbmE1x2OMHFuxJcegjBwJyqDfLvjfLK5vhaHpL0+EREsoGSLxGJS3D9m1B8EJTPBsogOBeKBxFcO7GyjS+3P7S9Gwi7z6NtCQVfpTpcEZGMpQH3IhKfVRdFLl/zL2h9YuWir9UR0OqIFAUlIpJ9dORLROIUjF4TKEthHCIi2U3Jl4iIiEgKKfkSkThFG6Vg+Py5KY0k0cxskJn9ZGazzeyqCPUtzGyiV/+pmfVKfZQi0lgo+RKR+OQ/Gbm83djUxpFgZpYDjAUOA3YCTjaznao1OwtY6ZzbDhgD3JbaKEWkMdGAexGJiy93L4IF38CqoRD4CXK2hvwH8Pnz0x1aQ/UFZjvn5gCY2bPAYOD7sDaDgVHe8xeA+83MnHMu2krnLFvLiQ99nJyIJeN8v2g1gP7PJS5KvkQkbj5/LnR8LN1hJNoWwLyw5fnAPtHaOOcCZrYK6AgUhzcys3OBcwHyum2brHhFJMslJPkys0FAEZADTHDO3VqtvgXwBLAXsBw40Tk3NxHbjqS0pJSrBt3E/J8X0qlHR26YfBVde3VO1uZERABwzo0HxgMUFha6ief1S3NEkioVR7z0f960PTc0vnYNHvOVaeMlvnjnG47pcCY/zZjN2pJ1zJ01j9O2uYDXH5marE2KSHZbAPQMW+7hlUVsY2Z+oB2hH5IiInWWiCNfSRkvAfUbM/HN9O/hpEE1yi95+zceC+hcfLbQ+AlJoc+A7c1sa0JJ1knAKdXaTAbOAD4GjgPeqa3/EhGJJhFXO0YaL7FFtDbOuQBQMV6iBjM718xmmtnMTZs21T2aGN1hYFOg7usTkUbN65MuBN4AfgCec859Z2ajzewor9kjQEczmw1cAtSYjkJEJF4ZN+C+oWMmDj7/7qh1j9z7V/ILsv7KrCZB4yeapnjHSySac24KMKVa2bVhz8uA41Mdl4g0TolIvuoyXmJ+MsZLBAIBJhVNwZfjI8fvozwQ4TYohhIvERERSbtEJF9pHS9x//AJvDz2jVrbXfrIsERsTkRERKRBGpx8eXPeVIyXyAEerRgvAcx0zk0mNF7iSW+8xApCCVqDff/xT1ETr21225Klvy2nS69O3DjlHxR0bZ+ITYqIiIg0SELGfKVrvMQtp94btW7j+gCTVjyW6E2KiIiINEhW39txzcrSqHWrilenMBIRERGR+GR18rVj3+2j1vU5YMcURiIiIiISn6xOvq576bKoddc+f0kKIxERERGJT1YnX7m5uTw5ZyztOrWtLMvv0o5nFo7H78+4KcxEREREMm+S1brq2qszLyx5JN1hiIiIiMQlq498iYiIiGQbJV8iIiIiKaTkS0RERCSFlHyJiIiIpJCSLxEREZEUUvIlIiIikkJKvkRERERSSMmXiIiISAop+RIRERFJoayf4b6hSopLKBo6gcCmci4aexadehSkOyQRERFpxJp08nX9cXfywX8/rVz+5JWZ7H5QH+6Yel2tr31t/Fs8Puo5cI4Trzyav158RDJDFRERkUaiyZ52/P7jn6okXhW+emcW01/4KOZrT+k1lHuGjmfl4hJWLlnFg5c8znFdz0pWqCIiItKINNnk67Yz7o9ad//wR6PWvTb+LZb9vrxG+aqlq3n6phcTEpuIiIg0Xk02+Vq3en3UurJ1G6LWPTn6+ah1L4x5tUExiYiISOPXZJOvg07eP2rd3ofuHrXOOZeMcERERKSJaLLJ1/ljhpDjr/n2zWdc/fSIqK875R/HRq07evjhCYlNREREGq8mm3wBvLruP/yh73aYGWaw9S5b8t+Sx/H7o18EOviCw+jQNb9GeZv2rTn9uuOTGa6IiIg0Ag2aasLMOgATgV7AXOAE59zKCO3KgW+9xd+dc0c1ZLuJ4vf7uf+TW+r8uokLH2biHS/x3B2TcUHHMRcfzmn/VOIlIiIitWvoPF9XAW875241s6u85SsjtFvvnIs+kCoLnXj50Zx4+dHpDkNERESyTENPOw4GHveePw4oGxERERGJoaHJVxfn3CLv+WKgS5R2uWY208w+MbOYCZqZneu1nbls2bIGhiciIiKSWWo97WhmU4GuEaquCV9wzjkzizYPw1bOuQVmtg3wjpl965z7NVJD59x4YDxAYWFhRs/rsGD2Im459V7KSssYcsNJ7H/MPukOSURERDJcrcmXc25gtDozW2Jm3Zxzi8ysG7A0yjoWeP/OMbNpwB5AxOQrU5WWlHL7mQ+wtmQt59x+GhNve6nK7Ymu/+ud5Hduy/OLH0ljlCIiIpLpGnracTJwhvf8DODl6g3MrL2ZtfCeFwD9ge8buN2UuveChzmmw5l8/PJnfPPe9wzf5+qI94UsWbqaovMfSkOEIlIfZtbBzN4ys1+8f9tHaVduZl95j8mpjlNEGpeGJl+3Ageb2S/AQG8ZMys0swlemx2BmWb2NfAucKtzLmuSr+LFK3ll3Jtxt3/jsWnJC0ZEEq3iiu3tgbe95UjWO+d29x4ZMVWOiGSvBk014ZxbDvw5QvlM4Gzv+UfALg3ZTjrdcvI9dWofLA8mKRIRSYLBwADv+ePANCJPlyMikjBNeob7eKxevqZO7Xfs1ztJkYhIEiT0im1drS0i8VDyVYtY93KsznzGLW9cU3tDEUkZM5tqZrMiPAaHt3POOSDWFduFwCnAPWa2baRGzrnxzrlC51xhp06dEvtGRKTRUPJViz+dtD957VpFb2CQ489hlwN3ZHLpE+Tm5qYuOBGplXNuoHOuT4THy8AS70pt4r1im9CpyT1SFL6INEJKvuIwaeXj9Bu8N2YRKh106J7P3dNGK/ESyT5N4optEcksSr7iNHrSFeQ0y4lYt+z35SybX5ziiEQkARr9FdsiknkaemPtJiWwsTxq3Uv3v845t56WwmhEpKGawhXbIpJ5dOQrQbbauWe6QxAREZEsoOSrDnY/qE/UujuHPJDCSERERCRbKfmqgzumXhd13JdzjjHnPZjiiERERCTbKPmqoxx/9I/s09e+TGEkIiIiko2UfNVRqzYto9a179ouhZGIiIhINlLyVUeXThgWtW70S1ekMBIRERHJRkq+6mjfI/bioL8dUKP8tOtOoFOPgjREJCIiItlEyVc9XP7vYXTonl+lbOXiFWmKRkRERLKJkq96OLbDmaxYWFKl7NWHpvLwVU+mKSIRERHJFkq+6mjON3NZX1oWse75O19JcTQiIiKSbZR81dG7z34Utc4FXQojERERkWyk5KuO9vnLHlHrzCyFkYiIiEg2UvJVR33674i/eeRZ7v98as2rIEVERETCKfmqh2fmP0iLVs2rlO32pz5c+fjwGm2di30qsrZ6kUi0X4mIZC9/ugPIRvkF+bxa+h9KS9ezYsEKtvzDFhHbjRo1ipKSEsaMGRPxlKRzjpEjR5Kfn8+oUaOSHLU0FtqvRESyW4OOfJnZ8Wb2nZkFzawwRrtBZvaTmc02s6sass1MkpfXMmri5ZyjpKSEoqIiRo4cWeNIRMUXZFFRESUlJTpSIXHRfiUikv0aeuRrFnAs8FC0BmaWA4wFDgbmA5+Z2WTn3PcN3HZGMzPGjBkDQFFREUDlkYrwL8gRI0ZEPYIhMvn3z7jl+5cpJwhA9xb5PH/HHYD2KxGRbNWg5Ms59wPUepVfX2C2c26O1/ZZYDDQqJMviJ6A6QtSqnvx908Y88MUNroAzc3PyB0Pp6B5W278flKVdgs3lPCnd27gfe1XIiJZKxVjvrYA5oUtzwf2ScF2M0L1BKziy1JfkFLh7h9e4dnfPq5c3ugC3Pb9ZHKIvG9sdAGmL/lB+5WISJaqdcyXmU01s1kRHoOTEZCZnWtmM81s5rJly5KxiZQLT8Aq6AtSKoQnXuHKiT5e66X5n2m/aoLuveBhDvGfwMG+4znEfwIPXvp4ukMSkXqoNflyzg10zvWJ8Hg5zm0sAHqGLffwyqJtb7xzrtA5V9ipU6c4N5HZKsbihIs0WFokXrvkb6n9qom59pjbeWXcm5V30nBBx4tjXuWW0+5Nc2QiUlepmOfrM2B7M9vazJoDJwGTU7DdjFB9EHQwGGTEiBFRr1YTiceZ2w7QftXEfPzyZxHL3/nP+wndzvQXPuL2M8fy9fTvErpeEdmsQWO+zOwY4D6gE/CamX3lnDvUzLoDE5xzhzvnAmZ2IfAGkAM86pxrEn/V0a4+i3YVpDRNLXx+NgQDNcpzfX52bteTz1f+X2WZAQ/ufY72qyamrKws6dtYNr+Yv209DFceStzfenwazXOb8+KKR8jNzU369kWqW7h2BRfMfJSF61eQg48jexRydZ+j0x1WQjT0asdJwKQI5QuBw8OWpwBTGrKtbBPrsn99UUq45w+8hMHTbq8ywsuA5w68hK65+QQCAV5f9BW98jrTJ7+n9qsmKBXJz+nbDa9MvCpsLNvIGdsMZ+LCh5O+fZFw80qL+esHd1cuBwgyaf4MZhT/wqQBl3PNV8/y1uJvKus7Ns/jlQOvwO/PjrnjsyPKLGRm5OfnR736LPyLMj8/X1+QTVjX3Hw+HXQzz/32MdOXfM+BXXbihK36Vdb7/X6O7Ll5DmPtV01TqzYtWbdmfY3yth3zGrzu0pJSAhtrHn0FWLG4pMHrF6mr82ZETvgXlK3k1A/v5ec1i6uUL99YysB3b2TawaNSEF3DKflKolGjRuGci/oFWPFFqS9IAThhq35Vkq5otF81Tf9Z8BDHF/y9SpLUPLc5/5k3rt7r/Pc/n+aZW17SGEHJOMUb10Stq554VVhXvpHfSpeyVV7nZIWVMEq+kqy2L0B9QUp9aL9qPD6Y9Cl3nDmWdWvW06Jlc8678wyOHHpIjXZ5eS15vewZZn34Ax/8dwYDTurPDntvF3PdZWVl3HH6WFYuXcUZo09ktwN3rqx7/q7JPH1zjVEjIhnBIMZkO9G9vfg7/r6dki8REYni+bsmM/7yJyuXN6zbyL3DHmb2l3MY+dDQiK/p039H+vTfsdZ1//ufT1dJri4bMIoOXfMrx2898o+n44rxuMuOjKudSCLt27E3Hy//uc6v27/TDkmIJvFSMdWEiIhE8PCVT0Usn/Lw21FfU9uVj845ysrKIh7VWrG4hJtPDV2MUb6pPOZ6cprlcPZtf+O820+P2U4kGYr2HlLjHh+1nR5vTg6923VLXlAJpCNfIiJpUjFhaiSL5y6la6/Np0+G7X0Fv3y+edqR9l3b8fTvD1a5umvUqFGUlJTQYUGPyNtzjoeeHstbX76Gn3ZRt/2v50Zy4HH71eWtiCTcpAMu4+j37wRgwVPvU15aRs/zBmJm9O2wLTNW/FrZtpWvOTu/WsyoT0YxatSoNEUcPyVfIiIZKK+gTeXzy/58fZXEC2Dl4lWc1P1cXlj6KBBKrEpKSigqKmKPLfrSwfWsMvbPOcfPfM08Nxv3vaM3u0UcG2g5psRLMsLN34WO3jrnKC8tY8nLMwHY8ryB3N/3LABWla2lVU5zLr/8coruG8uIESNiXpCUKXTaUUSaLDM73sy+M7OgmRXGaDfIzH4ys9lmdlWitt+qbcuI5Tl+H3l5m+u+fndWxHaritdQWrq+IkbGjBnDiBEj+HLBDH7m68rTNJWJF7PpyXZREy9/8xye+OX+hr4tkYSoOLJlZvQ8byBdBhey5OWZ/P7QVD4vDtW1bdEqlHhFmPswkyn5EpGmbBZwLDA9WgMzywHGAocBOwEnm9lOidj447PvxXw1vygemHlb3Ov4ecYvlc/DE7B5zK5MwGpNvAyeWTie18uerXKqUyRTVE/ArrzsipiTmWc6nXaUjFcaKOPSmU8wu3QxHZu34Y49/5YV87hI5nPO/QC1Ts3RF5jtnJvjtX0WGAx839Dt5xfk82bgOZ6/azIfvPgJuw7ow1k3n1KndfTuu32V5YoE7LM3vuKjH99jHrMBYh7xwkFB1/b1fh8iyZCDUR424URFAgbw9uP/xfd46PhRtiVeoCNfkuF+XrWIg6aO5suSuawJlDF33TKO/+AeHvt1WrpDk6ZjC2Be2PJ8r6wGMzvXzGaa2cxly5bFvYHjLz2Koo9ujpp49T18j4jlHbrlVzk9GRYHRUX3VCmLmngBHbdQ4iWZZ9j2h9YoC0/AKmRb4gVKviTDnf1p5Nm7H/jlzTqvq7hsNSe9fw/7/u8a9n/jWh74+Y2GhidZwMymmtmsCI/Bid6Wc268c67QOVfYqVOnhK33plf/we4H9alS1m3bzkxcEPkWLM45npryWJWy8DFg1d317vUJiVMkkU7b9kCGbV91wmHnHPMemlqlbOTIkVl3lwaddpSMVhaMfL85gC+Wz2HPjtvEtZ7FZSUcNe32yuWNLsBjc95j+pIfePaAixscp2Qu59zA2lvFtADoGbbcwytLqTumXhdXu/BxMBdccAGLn1/PjKXTK08/Dtr1KH7/YQHBQJAuW3fmjqnXapyXZKz9Cv5Q+WO7IvFa8vJMugwupOd5A+n/xnqKikJz12XTETAlX9IknPPxgxHL56xdyrzSYnrmFaQ4IskinwHbm9nWhJKuk4C6DcxKkUgDkOcNX8hF/f4BK2Ees5k2+00+X/wRbdq3qX2FImm2rnwDEDnxqhjfCGRdAqbTjpLRcn3NotbFe9QLYMmG1VHrin56vU4xSeNhZseY2XygH/Camb3hlXc3sykAzrkAcCHwBvAD8Jxz7rt0xRxNpMRr7aq1nLXjxawtWUdvdqMn2/HL2ln06bBX1p2mkaZp9w69oiZefvNVucK3qKgoa05BKvmSjDZhn8j3txu+/aA6rSfW76D85q3qtC5pPJxzk5xzPZxzLZxzXZxzh3rlC51zh4e1m+Kc6+2c29Y5d1P6Io7OzMjPz69y5dfVh91cpb4iAcvBzz1DH0pjtCLxO3GrfuTk5VZJvABu2S10ADo8AcvPz8+KI1867SgZrXe7bkwfeC0jP3+SX9YspmOLPO7c47TK04TvLf6eG2e9SGlgAwUt8rhrzzMi3ttrz/Zb8/nK/6tRDnDVjgkfdy2SFqNGjaoyu/ecb+ZWqTczervQVY+fvvZFGiIUqbvLdjqKfe/ejlFfv8Da8o10bdmOu/Y8jW3adK1sU5GAZUPiBUq+JAvk+nMZt885Ncrv+eE1nv7tw8rlJRtWc+rH93HjridySPfdqrQdt885/PHN61gf3FSl/PSt/1jl3ngi2S78y6dV21ZsXL8qYn37rvkpjUukIfbvvBNTD742ZptsSbxApx0li4UnXuGu++b5iOXvHXI91/U5jm1bd6Fvh215a8A1XPiHmvPIiDQWlz58ftS60S9dkcJIRCScfvJLVvp51aKodeUEo9b9pcee/KXHnskISSTj7HvEXgw87UCmPln17kmnXXcCnXroCl+RdFHyJVkpz98i3SGIZIUrHx/OpY+cz/N3vUKrvFwGX3BYukMSafKUfElW6t66AwZEuqC4pa951NcFAgEGT7+TZRs3Tz1xeq8DuXCHul09KZJN/H4/J195TLrDEBFPg8Z8mdnxZvadmQXNrDBGu7lm9q2ZfWVmMxuyTZEKd+15asTyiQdGn7H+oHdGV0m8AJ6YO53/zHk/obGJiIhE09AB97OAY4HptTUE/uSc2905FzVJE6mL/TvvxEcDR/OnzjvTs2VHjuu5LzMG3UzX3MhXcX21Ym7U2xWN1X0eRUQkRRp02tE59wNk1+Wd0rj4/X5u2/NvcbV9ZX70g66BGIP0RUQkdW6Z9RKT58+knCD5/laMKTyDnfM33171xm9fZPKCzyuXW+c056UDLqddbut0hFsvqZpqwgFvmtnnZnZurIZmdq6ZzTSzmcuWLUtReNIU7N95h6h1vphz4IuISCqc9P49TJo/o/Kq9ZLAOs78ZBxfLJ8DwMO/TK2SeAGsLd/IoGm3pDzWhqg1+TKzqWY2K8KjLtOC7++c2xM4DLjAzA6M1tA5N945V+icK+zUqVMdNiES25dRZrgH2L9T9MRMRESSr7hsNXPWLo1Yd+kXTwLw6K/TItaXE+S9xd8nK7SEq/W0o3NuYEM34pxb4P271MwmAX2Jb5yYSMK8NC/6acd564pTGImIiFT3yOx3o9atLd8AxJ7H8e3Fs/hj150SHlcyJP20o5m1NrM2Fc+BQwgN1BdJqaCL/ke7KViewkhERKS6rfJqn/g3J0ba8ueufRIZTlI1dKqJY8xsPtAPeM3M3vDKu5vZFK9ZF+ADM/samAG85pz7X0O2K1IfscZ8Det9SAojERGR6k7q1T9qXZ92oQH3Q7b5Y8T6HHxZc9QLGph8OecmOed6OOdaOOe6OOcO9coXOucO957Pcc7t5j12ds7dlIjARerqxl1OxG85Nco7NW/LwG67piEiEREJN3qXE2qUtc5pzqP9QvcpPa/3wfylW9VbxLXKac7rA65KSXyJohnupcnw+/1M//N1XD/rRaYt/Z4cjBN69WNYb91cW0QkEwzaYncGdunDPT9P4f/WLOX0bf7IPp22r9Lmut2O47rdjiMQCOD3Z2cak51Ri9ST3+/nht1PTHcYIiIShd/v57KdjoqrXbZK1TxfIiIiIoKSLxEREZGUUvIlIiIikkJKvkRERERSSMmXiIiISAop+RIRERFJIXPOpTuGqMxsGfBbA1ZRAGTaTfsyMSbIzLgUU/wyMa76xLSVc65TMoJJtQT0X4mSiftGPLIx7myMGRR3IsXVh2V08tVQZjbTOVeY7jjCZWJMkJlxKab4ZWJcmRhTU5St/w/ZGHc2xgyKOx102lFEREQkhZR8iYiIiKRQY0++xqc7gAgyMSbIzLgUU/wyMa5MjKkpytb/h2yMOxtjBsWdco16zJeIiIhIpmnsR75EREREMoqSLxEREZEUalTJl5kdb2bfmVnQzKJefmpmc83sWzP7ysxmZkhMg8zsJzObbWZXJTMmb3sdzOwtM/vF+7d9lHbl3uf0lZlNTlIsMd+7mbUws4le/adm1isZcdQxpiFmtizsszk7BTE9amZLzWxWlHozs3u9mL8xsz0zIKYBZrYq7HO6NtkxNXWZ2ufUJpP6pNpkYp8Vj0zs12qTif1eQjjnGs0D2BH4AzANKIzRbi5QkCkxATnAr8A2QHPga2CnJMd1O3CV9/wq4LYo7UqTHEet7x0YBjzoPT8JmJgBMQ0B7k/FPhS2zQOBPYFZUeoPB14HDNgX+DQDYhoAvJrKz6mpPzK1z4kj7ozok+KIM+P6rATGnfJ+LY64M67fS8SjUR35cs794Jz7Kd1xhIszpr7AbOfcHOfcRuBZYHCSQxsMPO49fxw4Osnbiyae9x4e6wvAn83M0hxTyjnnpgMrYjQZDDzhQj4B8s2sW5pjkhTL4D6nNpnSJ9UmE/useGTi/3mtMrHfS4RGlXzVgQPeNLPPzezcdAcDbAHMC1ue75UlUxfn3CLv+WKgS5R2uWY208w+MbNkdIbxvPfKNs65ALAK6JiEWOoSE8BfvcPcL5hZzyTGE6907Efx6GdmX5vZ62a2c7qDESAz95VM6ZNqk4l9VjyytV+rTSbuy7XypzuAujKzqUDXCFXXOOdejnM1+zvnFphZZ+AtM/vRy67TGVPCxYorfME558ws2pwjW3mf1TbAO2b2rXPu10THmoVeAZ5xzm0ws/MI/co9KM0xZaIvCO1DpWZ2OPASsH2aY8p6mdrn1EZ9UsZTv5YiWZd8OecGJmAdC7x/l5rZJEKHY+udfCUgpiTV4zoAAAHzSURBVAVA+C+MHl5Zg8SKy8yWmFk359wi7xDt0ijrqPis5pjZNGAPQuMGEiWe917RZr6Z+YF2wPIExlDnmJxz4dufQGi8SrolZT9qCOfc6rDnU8zsATMrcM5l2s1ws0qm9jm1yZI+qTaZ2GfFI1v7tdpkXL8XjyZ32tHMWptZm4rnwCFAxKsoUugzYHsz29rMmhMaoJnsq3gmA2d4z88AavxaNrP2ZtbCe14A9Ae+T3Ac8bz38FiPA95x3kjLJKk1pmpjCo4CfkhiPPGaDJzuXf2zL7Aq7DROWphZ14qxLmbWl1Cfk+4vIUlPn1ObTOmTapOJfVY8srVfq03G9XtxSfeI/0Q+gGMIne/dACwB3vDKuwNTvOfbELrK42vgO0KH6dMak9t8xcbPhH7BJTUmb3sdgbeBX4CpQAevvBCY4D3fD/jW+6y+Bc5KUiw13jswGjjKe54LPA/MBmYA26Tg86ktplu8/edr4F1ghxTE9AywCNjk7VNnAUOBoV69AWO9mL8lxhW/KYzpwrDP6RNgv2TH1NQfmdrnxBF3xvRJccSacX1WguJOeb8WR8wZ1+8l4qHbC4mIiIikUJM77SgiIiKSTkq+RERERFJIyZeIiIhICin5EhEREUkhJV8iIiIiKaTkS0RERCSFlHyJiIiIpND/A0PYCAFMIeLMAAAAAElFTkSuQmCC\n",
      "text/plain": [
       "<Figure size 720x288 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Visualize a few examples\n",
    "examples = next(radio.cfo_data_generator(OMEGA_TRAIN, SNR_TRAIN, batch_size=4, num_cpus=8))\n",
    "[preambles, preambles_conv], cfo_corrected = examples\n",
    "symbols, groundtruths = np.unique(preambles.view(np.complex),return_inverse=True)\n",
    "\n",
    "_, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))\n",
    "\n",
    "ax1.scatter(preambles_conv[...,0].flatten(),preambles_conv[...,1].flatten(), c=groundtruths)\n",
    "ax1.scatter(np.real(symbols), np.imag(symbols), marker='x', s=100, color='black')\n",
    "ax1.axhline()\n",
    "_ = ax1.axvline()\n",
    "ax1.set_title(\"Preamble_Conv before CFO Correction \")\n",
    "\n",
    "ax2.scatter(cfo_corrected[...,0].flatten(),cfo_corrected[...,1].flatten(), c=groundtruths)\n",
    "ax2.scatter(np.real(symbols), np.imag(symbols), marker='x', s=100, color='black')\n",
    "ax2.axhline()\n",
    "_ = ax2.axvline()\n",
    "_ = ax2.set_title(\"Preamble_Conv after CFO Correction\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "hLEGx68_eJ1j"
   },
   "source": [
    "## First Approach: Feedforward Network"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     },
     "base_uri": "https://localhost:8080/",
     "height": 648
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 482,
     "status": "ok",
     "timestamp": 1532646978288,
     "user": {
      "displayName": "Dat Nguyen",
      "photoUrl": "//lh3.googleusercontent.com/-irIcNYd-KIw/AAAAAAAAAAI/AAAAAAAAAEs/NlM8kG6RL4Q/s50-c-k-no/photo.jpg",
      "userId": "108917076199533451784"
     },
     "user_tz": 420
    },
    "id": "mtYdGSnieJ1n",
    "outputId": "906a9d75-754e-4519-bec3-0b879ed8a77a"
   },
   "outputs": [],
   "source": [
    "tf.keras.backend.clear_session()\n",
    "\n",
    "def cfo_network(preamble, preamble_conv, scope='CFONet'):\n",
    "    \"\"\"Build CFO Correct Network (or Graph).\"\"\"\n",
    "    with tf.name_scope(scope):\n",
    "        inputs = tf.keras.layers.concatenate([preamble, preamble_conv], axis=1)\n",
    "        inputs = tf.keras.layers.Flatten(name='Flatten')(inputs)\n",
    "        x = tf.keras.layers.Dense(100, 'tanh', name=scope+\"_dense_1\")(inputs)\n",
    "        x = tf.keras.layers.Dense(100, 'tanh', name=scope+\"_dense_2\")(x)\n",
    "        x = tf.keras.layers.Dense(100, 'tanh', name=scope+\"_dense_3\")(x)\n",
    "    cfo_est = tf.keras.layers.Dense(1, 'linear',name='CFOEstimate')(x)\n",
    "    return cfo_est\n",
    "\n",
    "\n",
    "def cfo_correction_func(kwargs):\n",
    "    \"\"\"Rotate packets given an omega estimate \n",
    "    \n",
    "    Arguments:\n",
    "        omega_estimate: tf.Tensor float32 - [batch, 1]\n",
    "        packets:        tf.Tensor float32 - [batch, (preamble_len + data_len), 2] \n",
    "        \n",
    "    Return:\n",
    "        rotated_packets: tf.Tensor float32 - [batch, (preamble_len + data_len), 2] \n",
    "    \"\"\" \n",
    "    # Because of Lambda Layer, we need to pass arguments as Kwargs\n",
    "    omega_estimate, packets = kwargs[0], kwargs[1]\n",
    "    # Build rotation matrix\n",
    "    with tf.name_scope('rotation_matrix'):\n",
    "        packet_len      = tf.cast(tf.shape(packets)[1], tf.float32) # preamble_len + data_len\n",
    "        rotation_matrix = tf.exp(tf.complex(0.0,-1.0* mega_estimate* tf.range(packet_len)))\n",
    "    # CFO Correction\n",
    "    with tf.name_scope('cfo_correction'):\n",
    "        rotated_packets = tf.complex(packets[..., 0], packets[...,1]) * rotation_matrix\n",
    "    # Encode complex packets into 2D array\n",
    "    corrected_preambe = tf.stack([tf.real(rotated_packets),\n",
    "                                  tf.imag(rotated_packets)], \n",
    "                                  axis=-1)\n",
    "    \n",
    "    return corrected_preambe"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Build CFO Correction Net"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Number of training parameters: 36401\n"
     ]
    },
    {
     "data": {
      "image/svg+xml": [
       "<svg height=\"636pt\" viewBox=\"0.00 0.00 647.50 636.00\" width=\"648pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g class=\"graph\" id=\"graph0\" transform=\"scale(1 1) rotate(0) translate(4 632)\">\n",
       "<title>G</title>\n",
       "<polygon fill=\"white\" points=\"-4,4 -4,-632 643.5,-632 643.5,4 -4,4\" stroke=\"none\"/>\n",
       "<!-- 139814779376696 -->\n",
       "<g class=\"node\" id=\"node1\"><title>139814779376696</title>\n",
       "<polygon fill=\"none\" points=\"0,-581.5 0,-627.5 281,-627.5 281,-581.5 0,-581.5\" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"68\" y=\"-600.8\">Preamble: InputLayer</text>\n",
       "<polyline fill=\"none\" points=\"136,-581.5 136,-627.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"163.5\" y=\"-612.3\">input:</text>\n",
       "<polyline fill=\"none\" points=\"136,-604.5 191,-604.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"163.5\" y=\"-589.3\">output:</text>\n",
       "<polyline fill=\"none\" points=\"191,-581.5 191,-627.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"236\" y=\"-612.3\">(None, 40, 2)</text>\n",
       "<polyline fill=\"none\" points=\"191,-604.5 281,-604.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"236\" y=\"-589.3\">(None, 40, 2)</text>\n",
       "</g>\n",
       "<!-- 139816946814704 -->\n",
       "<g class=\"node\" id=\"node3\"><title>139816946814704</title>\n",
       "<polygon fill=\"none\" points=\"51.5,-498.5 51.5,-544.5 441.5,-544.5 441.5,-498.5 51.5,-498.5\" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"129\" y=\"-517.8\">concatenate: Concatenate</text>\n",
       "<polyline fill=\"none\" points=\"206.5,-498.5 206.5,-544.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"234\" y=\"-529.3\">input:</text>\n",
       "<polyline fill=\"none\" points=\"206.5,-521.5 261.5,-521.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"234\" y=\"-506.3\">output:</text>\n",
       "<polyline fill=\"none\" points=\"261.5,-498.5 261.5,-544.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"351.5\" y=\"-529.3\">[(None, 40, 2), (None, 40, 2)]</text>\n",
       "<polyline fill=\"none\" points=\"261.5,-521.5 441.5,-521.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"351.5\" y=\"-506.3\">(None, 80, 2)</text>\n",
       "</g>\n",
       "<!-- 139814779376696&#45;&gt;139816946814704 -->\n",
       "<g class=\"edge\" id=\"edge1\"><title>139814779376696-&gt;139816946814704</title>\n",
       "<path d=\"M169.465,-581.366C181.851,-571.902 196.459,-560.739 209.606,-550.693\" fill=\"none\" stroke=\"black\"/>\n",
       "<polygon fill=\"black\" points=\"211.749,-553.46 217.57,-544.607 207.499,-547.898 211.749,-553.46\" stroke=\"black\"/>\n",
       "</g>\n",
       "<!-- 139816946991792 -->\n",
       "<g class=\"node\" id=\"node2\"><title>139816946991792</title>\n",
       "<polygon fill=\"none\" points=\"299.5,-581.5 299.5,-627.5 639.5,-627.5 639.5,-581.5 299.5,-581.5\" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"397\" y=\"-600.8\">PreambleConvolved: InputLayer</text>\n",
       "<polyline fill=\"none\" points=\"494.5,-581.5 494.5,-627.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"522\" y=\"-612.3\">input:</text>\n",
       "<polyline fill=\"none\" points=\"494.5,-604.5 549.5,-604.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"522\" y=\"-589.3\">output:</text>\n",
       "<polyline fill=\"none\" points=\"549.5,-581.5 549.5,-627.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"594.5\" y=\"-612.3\">(None, 40, 2)</text>\n",
       "<polyline fill=\"none\" points=\"549.5,-604.5 639.5,-604.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"594.5\" y=\"-589.3\">(None, 40, 2)</text>\n",
       "</g>\n",
       "<!-- 139816946991792&#45;&gt;139816946814704 -->\n",
       "<g class=\"edge\" id=\"edge2\"><title>139816946991792-&gt;139816946814704</title>\n",
       "<path d=\"M408.859,-581.473C380.346,-571.117 346.186,-558.709 316.572,-547.952\" fill=\"none\" stroke=\"black\"/>\n",
       "<polygon fill=\"black\" points=\"317.723,-544.647 307.129,-544.522 315.333,-551.226 317.723,-544.647\" stroke=\"black\"/>\n",
       "</g>\n",
       "<!-- 139816946950440 -->\n",
       "<g class=\"node\" id=\"node9\"><title>139816946950440</title>\n",
       "<polygon fill=\"none\" points=\"200,-0.5 200,-46.5 567,-46.5 567,-0.5 200,-0.5\" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"276.5\" y=\"-19.8\">CFOCorrection: Lambda</text>\n",
       "<polyline fill=\"none\" points=\"353,-0.5 353,-46.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"380.5\" y=\"-31.3\">input:</text>\n",
       "<polyline fill=\"none\" points=\"353,-23.5 408,-23.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"380.5\" y=\"-8.3\">output:</text>\n",
       "<polyline fill=\"none\" points=\"408,-0.5 408,-46.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"487.5\" y=\"-31.3\">[(None, 1), (None, 40, 2)]</text>\n",
       "<polyline fill=\"none\" points=\"408,-23.5 567,-23.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"487.5\" y=\"-8.3\">(None, 40, 2)</text>\n",
       "</g>\n",
       "<!-- 139816946991792&#45;&gt;139816946950440 -->\n",
       "<g class=\"edge\" id=\"edge9\"><title>139816946991792-&gt;139816946950440</title>\n",
       "<path d=\"M469.5,-581.483C469.5,-550.005 469.5,-490.323 469.5,-439.5 469.5,-439.5 469.5,-439.5 469.5,-188.5 469.5,-139.988 465.616,-125.093 441.5,-83 435.39,-72.3355 426.846,-62.2387 418.191,-53.5301\" fill=\"none\" stroke=\"black\"/>\n",
       "<polygon fill=\"black\" points=\"420.579,-50.9703 410.946,-46.5595 415.726,-56.0149 420.579,-50.9703\" stroke=\"black\"/>\n",
       "</g>\n",
       "<!-- 139816946811680 -->\n",
       "<g class=\"node\" id=\"node4\"><title>139816946811680</title>\n",
       "<polygon fill=\"none\" points=\"130.5,-415.5 130.5,-461.5 374.5,-461.5 374.5,-415.5 130.5,-415.5\" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"180\" y=\"-434.8\">Flatten: Flatten</text>\n",
       "<polyline fill=\"none\" points=\"229.5,-415.5 229.5,-461.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"257\" y=\"-446.3\">input:</text>\n",
       "<polyline fill=\"none\" points=\"229.5,-438.5 284.5,-438.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"257\" y=\"-423.3\">output:</text>\n",
       "<polyline fill=\"none\" points=\"284.5,-415.5 284.5,-461.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"329.5\" y=\"-446.3\">(None, 80, 2)</text>\n",
       "<polyline fill=\"none\" points=\"284.5,-438.5 374.5,-438.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"329.5\" y=\"-423.3\">(None, 160)</text>\n",
       "</g>\n",
       "<!-- 139816946814704&#45;&gt;139816946811680 -->\n",
       "<g class=\"edge\" id=\"edge3\"><title>139816946814704-&gt;139816946811680</title>\n",
       "<path d=\"M248.14,-498.366C248.748,-490.152 249.451,-480.658 250.113,-471.725\" fill=\"none\" stroke=\"black\"/>\n",
       "<polygon fill=\"black\" points=\"253.614,-471.838 250.862,-461.607 246.633,-471.321 253.614,-471.838\" stroke=\"black\"/>\n",
       "</g>\n",
       "<!-- 139816946812296 -->\n",
       "<g class=\"node\" id=\"node5\"><title>139816946812296</title>\n",
       "<polygon fill=\"none\" points=\"130,-332.5 130,-378.5 423,-378.5 423,-332.5 130,-332.5\" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"207.5\" y=\"-351.8\">CFONet_dense_1: Dense</text>\n",
       "<polyline fill=\"none\" points=\"285,-332.5 285,-378.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"312.5\" y=\"-363.3\">input:</text>\n",
       "<polyline fill=\"none\" points=\"285,-355.5 340,-355.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"312.5\" y=\"-340.3\">output:</text>\n",
       "<polyline fill=\"none\" points=\"340,-332.5 340,-378.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"381.5\" y=\"-363.3\">(None, 160)</text>\n",
       "<polyline fill=\"none\" points=\"340,-355.5 423,-355.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"381.5\" y=\"-340.3\">(None, 100)</text>\n",
       "</g>\n",
       "<!-- 139816946811680&#45;&gt;139816946812296 -->\n",
       "<g class=\"edge\" id=\"edge4\"><title>139816946811680-&gt;139816946812296</title>\n",
       "<path d=\"M259.058,-415.366C261.519,-407.062 264.366,-397.451 267.038,-388.434\" fill=\"none\" stroke=\"black\"/>\n",
       "<polygon fill=\"black\" points=\"270.465,-389.189 269.95,-378.607 263.753,-387.2 270.465,-389.189\" stroke=\"black\"/>\n",
       "</g>\n",
       "<!-- 139816946683240 -->\n",
       "<g class=\"node\" id=\"node6\"><title>139816946683240</title>\n",
       "<polygon fill=\"none\" points=\"136,-249.5 136,-295.5 429,-295.5 429,-249.5 136,-249.5\" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"213.5\" y=\"-268.8\">CFONet_dense_2: Dense</text>\n",
       "<polyline fill=\"none\" points=\"291,-249.5 291,-295.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"318.5\" y=\"-280.3\">input:</text>\n",
       "<polyline fill=\"none\" points=\"291,-272.5 346,-272.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"318.5\" y=\"-257.3\">output:</text>\n",
       "<polyline fill=\"none\" points=\"346,-249.5 346,-295.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"387.5\" y=\"-280.3\">(None, 100)</text>\n",
       "<polyline fill=\"none\" points=\"346,-272.5 429,-272.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"387.5\" y=\"-257.3\">(None, 100)</text>\n",
       "</g>\n",
       "<!-- 139816946812296&#45;&gt;139816946683240 -->\n",
       "<g class=\"edge\" id=\"edge5\"><title>139816946812296-&gt;139816946683240</title>\n",
       "<path d=\"M278.14,-332.366C278.748,-324.152 279.451,-314.658 280.113,-305.725\" fill=\"none\" stroke=\"black\"/>\n",
       "<polygon fill=\"black\" points=\"283.614,-305.838 280.862,-295.607 276.633,-305.321 283.614,-305.838\" stroke=\"black\"/>\n",
       "</g>\n",
       "<!-- 139816946479400 -->\n",
       "<g class=\"node\" id=\"node7\"><title>139816946479400</title>\n",
       "<polygon fill=\"none\" points=\"142,-166.5 142,-212.5 435,-212.5 435,-166.5 142,-166.5\" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"219.5\" y=\"-185.8\">CFONet_dense_3: Dense</text>\n",
       "<polyline fill=\"none\" points=\"297,-166.5 297,-212.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"324.5\" y=\"-197.3\">input:</text>\n",
       "<polyline fill=\"none\" points=\"297,-189.5 352,-189.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"324.5\" y=\"-174.3\">output:</text>\n",
       "<polyline fill=\"none\" points=\"352,-166.5 352,-212.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"393.5\" y=\"-197.3\">(None, 100)</text>\n",
       "<polyline fill=\"none\" points=\"352,-189.5 435,-189.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"393.5\" y=\"-174.3\">(None, 100)</text>\n",
       "</g>\n",
       "<!-- 139816946683240&#45;&gt;139816946479400 -->\n",
       "<g class=\"edge\" id=\"edge6\"><title>139816946683240-&gt;139816946479400</title>\n",
       "<path d=\"M284.14,-249.366C284.748,-241.152 285.451,-231.658 286.113,-222.725\" fill=\"none\" stroke=\"black\"/>\n",
       "<polygon fill=\"black\" points=\"289.614,-222.838 286.862,-212.607 282.633,-222.321 289.614,-222.838\" stroke=\"black\"/>\n",
       "</g>\n",
       "<!-- 139816955781360 -->\n",
       "<g class=\"node\" id=\"node8\"><title>139816955781360</title>\n",
       "<polygon fill=\"none\" points=\"162.5,-83.5 162.5,-129.5 432.5,-129.5 432.5,-83.5 162.5,-83.5\" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"228.5\" y=\"-102.8\">CFOEstimate: Dense</text>\n",
       "<polyline fill=\"none\" points=\"294.5,-83.5 294.5,-129.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"322\" y=\"-114.3\">input:</text>\n",
       "<polyline fill=\"none\" points=\"294.5,-106.5 349.5,-106.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"322\" y=\"-91.3\">output:</text>\n",
       "<polyline fill=\"none\" points=\"349.5,-83.5 349.5,-129.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"391\" y=\"-114.3\">(None, 100)</text>\n",
       "<polyline fill=\"none\" points=\"349.5,-106.5 432.5,-106.5 \" stroke=\"black\"/>\n",
       "<text font-family=\"Times,serif\" font-size=\"14.00\" text-anchor=\"middle\" x=\"391\" y=\"-91.3\">(None, 1)</text>\n",
       "</g>\n",
       "<!-- 139816946479400&#45;&gt;139816955781360 -->\n",
       "<g class=\"edge\" id=\"edge7\"><title>139816946479400-&gt;139816955781360</title>\n",
       "<path d=\"M290.959,-166.366C291.872,-158.152 292.927,-148.658 293.919,-139.725\" fill=\"none\" stroke=\"black\"/>\n",
       "<polygon fill=\"black\" points=\"297.418,-139.932 295.044,-129.607 290.461,-139.159 297.418,-139.932\" stroke=\"black\"/>\n",
       "</g>\n",
       "<!-- 139816955781360&#45;&gt;139816946950440 -->\n",
       "<g class=\"edge\" id=\"edge8\"><title>139816955781360-&gt;139816946950440</title>\n",
       "<path d=\"M321,-83.3664C330.765,-74.1695 342.231,-63.3694 352.659,-53.5482\" fill=\"none\" stroke=\"black\"/>\n",
       "<polygon fill=\"black\" points=\"355.149,-56.011 360.029,-46.6068 350.349,-50.9153 355.149,-56.011\" stroke=\"black\"/>\n",
       "</g>\n",
       "</g>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Think of keras.Input as tf.placeholder\n",
    "preamble       = tf.keras.layers.Input(shape=(PREAMBLE_LEN, 2), name='Preamble')\n",
    "preamble_conv  = tf.keras.layers.Input(shape=(PREAMBLE_LEN, 2), name='PreambleConvolved')\n",
    "\n",
    "# Build tensorflow graph\n",
    "cfo_estimate           = cfo_network(preamble, preamble_conv)\n",
    "cfo_corrected_preamble = tf.keras.layers.Lambda(cfo_correction_func,\n",
    "                                                name='CFOCorrection')([cfo_estimate, preamble_conv])\n",
    "\n",
    "feed_forward_model = tf.keras.Model(inputs=[preamble, preamble_conv], \n",
    "                                    outputs=cfo_corrected_preamble)\n",
    "\n",
    "print(\"Number of training parameters: %d\" % feed_forward_model.count_params())\n",
    "SVG(model_to_dot(feed_forward_model, show_shapes=True, show_layer_names=True).create(prog='dot', format='svg'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Training Feedforward model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     },
     "base_uri": "https://localhost:8080/",
     "height": 153
    },
    "colab_type": "code",
    "id": "JZPWrqfOeJ1y",
    "outputId": "dc02a0bc-df04-4108-d3ff-c94c45c3394f"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/30\n",
      "1000/1000 [==============================] - 92s 92ms/step - loss: 0.0041 - val_loss: 0.0037\n",
      "Epoch 2/30\n",
      "1000/1000 [==============================] - 88s 88ms/step - loss: 0.0020 - val_loss: 0.0025\n",
      "Epoch 3/30\n",
      "1000/1000 [==============================] - 86s 86ms/step - loss: 0.0017 - val_loss: 0.0025\n",
      "Epoch 4/30\n",
      "1000/1000 [==============================] - 87s 87ms/step - loss: 0.0014 - val_loss: 0.0032\n",
      "Epoch 5/30\n",
      "1000/1000 [==============================] - 87s 87ms/step - loss: 0.0013 - val_loss: 0.0024\n",
      "Epoch 6/30\n",
      "1000/1000 [==============================] - 86s 86ms/step - loss: 0.0013 - val_loss: 0.0025\n",
      "Epoch 7/30\n",
      "1000/1000 [==============================] - 86s 86ms/step - loss: 0.0016 - val_loss: 0.0025\n",
      "Epoch 8/30\n",
      "1000/1000 [==============================] - 86s 86ms/step - loss: 0.0014 - val_loss: 0.0022\n",
      "Epoch 9/30\n",
      "1000/1000 [==============================] - 85s 85ms/step - loss: 0.0014 - val_loss: 0.0023\n",
      "Epoch 10/30\n",
      "1000/1000 [==============================] - 84s 84ms/step - loss: 0.0013 - val_loss: 0.0029\n",
      "Epoch 11/30\n",
      "1000/1000 [==============================] - 84s 84ms/step - loss: 0.0014 - val_loss: 0.0021\n",
      "Epoch 12/30\n",
      "1000/1000 [==============================] - 84s 84ms/step - loss: 0.0012 - val_loss: 0.0022\n",
      "Epoch 13/30\n",
      "1000/1000 [==============================] - 84s 84ms/step - loss: 0.0013 - val_loss: 0.0022\n",
      "Epoch 14/30\n",
      "1000/1000 [==============================] - 86s 86ms/step - loss: 0.0012 - val_loss: 0.0019\n",
      "Epoch 15/30\n",
      "1000/1000 [==============================] - 88s 88ms/step - loss: 9.8285e-04 - val_loss: 0.0027\n",
      "Epoch 16/30\n",
      "1000/1000 [==============================] - 87s 87ms/step - loss: 0.0011 - val_loss: 0.0019\n",
      "Epoch 17/30\n",
      "1000/1000 [==============================] - 88s 88ms/step - loss: 0.0010 - val_loss: 0.0019\n",
      "Epoch 18/30\n",
      "1000/1000 [==============================] - 88s 88ms/step - loss: 0.0010 - val_loss: 0.0019\n",
      "Epoch 19/30\n",
      "1000/1000 [==============================] - 85s 85ms/step - loss: 0.0012 - val_loss: 0.0017\n",
      "Epoch 20/30\n",
      "1000/1000 [==============================] - 86s 86ms/step - loss: 0.0010 - val_loss: 0.0017\n",
      "Epoch 21/30\n",
      "1000/1000 [==============================] - 87s 87ms/step - loss: 8.9858e-04 - val_loss: 0.0015\n",
      "Epoch 22/30\n",
      "1000/1000 [==============================] - 88s 88ms/step - loss: 8.7111e-04 - val_loss: 0.0020\n",
      "Epoch 23/30\n",
      "1000/1000 [==============================] - 88s 88ms/step - loss: 9.5159e-04 - val_loss: 0.0030\n",
      "Epoch 24/30\n",
      "1000/1000 [==============================] - 88s 88ms/step - loss: 8.9152e-04 - val_loss: 0.0016\n",
      "Epoch 25/30\n",
      "1000/1000 [==============================] - 89s 89ms/step - loss: 8.9161e-04 - val_loss: 0.0017\n",
      "Epoch 26/30\n",
      "1000/1000 [==============================] - 87s 87ms/step - loss: 7.2711e-04 - val_loss: 0.0017\n",
      "Epoch 27/30\n",
      "1000/1000 [==============================] - 87s 87ms/step - loss: 9.8202e-04 - val_loss: 0.0016\n",
      "Epoch 28/30\n",
      "1000/1000 [==============================] - 87s 87ms/step - loss: 7.4002e-04 - val_loss: 0.0018\n",
      "Epoch 29/30\n",
      "1000/1000 [==============================] - 86s 86ms/step - loss: 6.7593e-04 - val_loss: 0.0017\n",
      "Epoch 30/30\n",
      "1000/1000 [==============================] - 87s 87ms/step - loss: 8.6197e-04 - val_loss: 0.0031\n"
     ]
    }
   ],
   "source": [
    "feed_forward_model.compile('adam','mse')\n",
    "\n",
    "# Load pretrained-weights from cheating with omegas\n",
    "feed_forward_model.load_weights('../models/cfo_nn.hdf5')\n",
    "\n",
    "history = feed_forward_model.fit_generator(\n",
    "    generator=training_generator,\n",
    "    validation_data=validation_generator,\n",
    "    steps_per_epoch=1000,\n",
    "    validation_steps=100,\n",
    "    callbacks=[tf.keras.callbacks.ModelCheckpoint('nn.hdf5', save_best_only=True)],\n",
    "    epochs=30, \n",
    "    workers=8)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Source: https://machinelearningmastery.com/display-deep-learning-model-training-history-in-keras/\n",
    "plt.plot(history.history['loss'])\n",
    "plt.plot(history.history['val_loss'])\n",
    "plt.title('model loss')\n",
    "plt.ylabel('Loss')\n",
    "plt.xlabel('Epoch')\n",
    "plt.legend(['Training', 'Validation'], loc='upper left')\n",
    "plt.grid()\n",
    "plt.semilogy()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "c2pXMQDOeJ2R"
   },
   "source": [
    "## Second Approach: Recurrent Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     },
     "base_uri": "https://localhost:8080/",
     "height": 556
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1767,
     "status": "ok",
     "timestamp": 1532642820304,
     "user": {
      "displayName": "Dat Nguyen",
      "photoUrl": "//lh3.googleusercontent.com/-irIcNYd-KIw/AAAAAAAAAAI/AAAAAAAAAEs/NlM8kG6RL4Q/s50-c-k-no/photo.jpg",
      "userId": "108917076199533451784"
     },
     "user_tz": 420
    },
    "id": "8sY3eOBVeJ2V",
    "outputId": "e3935bdc-1867-4c74-c90f-e372e6fc7444"
   },
   "outputs": [],
   "source": [
    "from tensorflow.keras.layers import Input, Bidirectional, GRU, TimeDistributed, Dense\n",
    "\n",
    "def cfo_rnn_network(preamble, preamble_conv, scope=\"CFONet\"):\n",
    "    \"\"\"\n",
    "    Arguments:\n",
    "        preamble :     tf.Tensor float32 -  [batch, preamble_length, 2]\n",
    "        preamble_conv: tf.Tensor float32 -  [batch, preamble_length, 2]\n",
    "    Return:\n",
    "        cfo_est: tf.Tensor float32 - [batch_size, 1]\n",
    "    \"\"\"\n",
    "    with tf.name_scope(scope):\n",
    "        inputs = tf.keras.layers.concatenate([preamble, preamble_conv], axis=-1, \n",
    "                                             name='Preamble_PreambleConv')\n",
    "        x = Bidirectional(GRU(40, 'selu', return_sequences=True), name=scope+\"_GRU_1\")(inputs)\n",
    "        x = Bidirectional(GRU(40, 'selu', return_sequences=False),name=scope+\"_GRU_2\")(x)\n",
    "        cfo_est = Dense(1, 'linear', name='CFOEstimate')(x)\n",
    "    return cfo_est\n",
    "\n",
    "# ############################\n",
    "# Construct Recurrent Model\n",
    "# ############################\n",
    "preamble        = Input(shape=(None, 2), name='preamble')\n",
    "preamble_conv   = Input(shape=(None, 2), name='preamble_conv')\n",
    "\n",
    "cfo_estimate  = cfo_rnn_network(preamble, preamble_conv)\n",
    "cfo_corrected_preamble = tf.keras.layers.Lambda(cfo_correction_func,\n",
    "                                                name='CFOCorrection')([cfo_estimate, preamble_conv])\n",
    "\n",
    "recurrent_model = tf.keras.Model([preamble, preamble_conv], cfo_corrected_preamble)\n",
    "\n",
    "print(\"Number of training parameters: %d\" % recurrent_model.count_params())\n",
    "tf.keras.utils.plot_model(recurrent_model, \"cfo_rnn_model.svg\", show_shapes=True, show_layer_names=True)\n",
    "SVG(\"cfo_rnn_model.svg\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Training Recurrent model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     },
     "base_uri": "https://localhost:8080/",
     "height": 220
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 521112,
     "status": "ok",
     "timestamp": 1532643455572,
     "user": {
      "displayName": "Dat Nguyen",
      "photoUrl": "//lh3.googleusercontent.com/-irIcNYd-KIw/AAAAAAAAAAI/AAAAAAAAAEs/NlM8kG6RL4Q/s50-c-k-no/photo.jpg",
      "userId": "108917076199533451784"
     },
     "user_tz": 420
    },
    "id": "nCkLrXmGeJ2o",
    "outputId": "5822ac06-b552-4bfc-c64a-0d976ae2e151"
   },
   "outputs": [],
   "source": [
    "recurrent_model.compile('adam','mse')\n",
    "\n",
    "# Load a pretrained model from cheating with real omegas\n",
    "recurrent_model.load_weights('../models/cfo_rnn.hdf5')  \n",
    "\n",
    "history = recurrent_model.fit_generator(\n",
    "    generator=training_generator,\n",
    "    validation_data=validation_generator,\n",
    "    steps_per_epoch=1000,\n",
    "    validation_steps=100,\n",
    "    callbacks=[tf.keras.callbacks.ModelCheckpoint('rnn.hdf5', save_best_only=True)],\n",
    "    epochs=20, \n",
    "    workers=8)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Source: https://machinelearningmastery.com/display-deep-learning-model-training-history-in-keras/\n",
    "plt.plot(history.history['loss'])\n",
    "plt.plot(history.history['val_loss'])\n",
    "plt.title('model loss')\n",
    "plt.ylabel('Loss')\n",
    "plt.xlabel('Epoch')\n",
    "plt.legend(['Training', 'Validation'], loc='upper left')\n",
    "plt.grid()\n",
    "plt.semilogy()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Evaluate models on different Omegas and SNRs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "autoexec": {
      "startup": false,
      "wait_interval": 0
     },
     "base_uri": "https://localhost:8080/",
     "height": 86
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1192,
     "status": "ok",
     "timestamp": 1532643520537,
     "user": {
      "displayName": "Dat Nguyen",
      "photoUrl": "//lh3.googleusercontent.com/-irIcNYd-KIw/AAAAAAAAAAI/AAAAAAAAAEs/NlM8kG6RL4Q/s50-c-k-no/photo.jpg",
      "userId": "108917076199533451784"
     },
     "user_tz": 420
    },
    "id": "YE1Eid_jeJ20",
    "outputId": "979f2626-d17e-47c4-c9f0-2e268b8d8d59"
   },
   "outputs": [],
   "source": [
    "# Load best trained models\n",
    "import tensorflow as tf\n",
    "\n",
    "feedforward = tf.keras.models.load_model('nn.hdf5', custom_objects={\"tf\": tf})\n",
    "recurrent_nn = tf.keras.models.load_model('rnn.hdf5',custom_objects={\"tf\": tf})\n",
    "\n",
    "radio = RadioData(data_len    =DATA_LEN, \n",
    "                  preamble_len=PREAMBLE_LEN,\n",
    "                  channels_len=CHANNEL_LEN,\n",
    "                  modulation_scheme='QPSK')\n",
    "\n",
    "logs = {}\n",
    "for w in [1/20, 1/50, 1/100]:\n",
    "    logs[w] = []\n",
    "    print('[Omega]: %f' % w)\n",
    "    for snr in [10.0, 15.0, 20.0, 25.0]:\n",
    "        # Define testing data\n",
    "        test_inputs, test_labels = next(cfo_data_genenerator(radio, w, snr, batch_size=100, seed=2020))\n",
    "        \n",
    "        # Make Predictions\n",
    "        rnn_predictions = recurrent_nn.predict(test_inputs)\n",
    "        nn_predictions  = feedforward.predict(test_inputs)\n",
    "        \n",
    "        # Compute MSE\n",
    "        nn_mse = mean_squared_error(test_labels.flatten(), nn_predictions.flatten())\n",
    "        rnn_mse =mean_squared_error(test_labels.flatten(), rnn_predictions.flatten())\n",
    "        \n",
    "        print('\\t[SNR]: %.2f || Feedforward MSE: %.8f || RecurrentNN MSE: %.8f'% (snr, nn_mse, rnn_mse))\n",
    "        logs[w].append([nn_mse, rnn_mse])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "k= np.random.randint(0, len(test_labels) - 5)\n",
    "print('K=%d'%k)\n",
    "print(\"True CFO Rate:         \", test_labels.flatten()[k:k+6])\n",
    "print(\"RNN Estimate CFO Rate: \", rnn_predictions.flatten()[k:k+6].T)\n",
    "print(\"NN  Estimate CFO Rate: \", nn_predictions.flatten()[k:k+6].T)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Evaluate on longer preamble"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "radio = RadioData(data_len    =DATA_LEN, \n",
    "                  preamble_len=2*PREAMBLE_LEN,  # 80\n",
    "                  channels_len=CHANNEL_LEN,\n",
    "                  modulation_scheme='QPSK')\n",
    "logs = {}\n",
    "for w in [1/20, 1/50, 1/100]:\n",
    "    logs[w] = []\n",
    "    print('[Omega]: %f' % w)\n",
    "    for snr in [10.0, 15.0, 20.0, 25.0]:\n",
    "        # Define testing data\n",
    "        test_inputs, test_labels = next(cfo_data_genenerator(radio, w, snr, batch_size=100, seed=2020))\n",
    "        \n",
    "        # Make Predictions\n",
    "        rnn_predictions = recurrent_nn.predict(test_inputs)\n",
    "        \n",
    "        # Compute MSE\n",
    "        rnn_mse =mean_squared_error(test_labels.flatten(), rnn_predictions.flatten())\n",
    "        \n",
    "        print('\\t[SNR]: %.2f  RecurrentNN MSE: %.8f'% (snr, rnn_mse))\n",
    "        logs[w].append([nn_mse, rnn_mse])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "collapsed_sections": [],
   "default_view": {},
   "name": "Copy of TrainCFONet.ipynb",
   "provenance": [
    {
     "file_id": "14VQfWvCqZQ1Ui16uXdtbU2Hpp3nDHGIu",
     "timestamp": 1532642180478
    }
   ],
   "version": "0.3.2",
   "views": {}
  },
  "kernelspec": {
   "display_name": "Radio ML",
   "language": "python",
   "name": "radioml"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
